package copier

// copy from https://github.com/jinzhu/copier

import (
    "database/sql"
    "errors"
    "reflect"
)

func skip_field(n string) bool {
    if n == "ID" || n == "CreatedAt" || n == "UpdatedAt" || n == "DeletedAt" {
        return true
    }
    return false
}

func GormCopy(toValue interface{}, fromValue interface{}) {
    _ = _copy(toValue, fromValue)
}

// Copy copy things
func _copy(toValue interface{}, fromValue interface{}) (err error) {
    var (
        isSlice bool
        amount  = 1
        from    = indirect(reflect.ValueOf(fromValue))
        to      = indirect(reflect.ValueOf(toValue))
    )

    if !to.CanAddr() {
        return errors.New("copy to value is unaddressable")
    }

    // Return is from value is invalid
    if !from.IsValid() {
        return
    }

    fromType := indirectType(from.Type())
    toType := indirectType(to.Type())

    // Just set it if possible to assign
    // And need to do copy anyway if the type is struct
    if fromType.Kind() != reflect.Struct && from.Type().AssignableTo(to.Type()) {
        to.Set(from)
        return
    }

    if fromType.Kind() != reflect.Struct || toType.Kind() != reflect.Struct {
        return
    }

    if to.Kind() == reflect.Slice {
        isSlice = true
        if from.Kind() == reflect.Slice {
            amount = from.Len()
        }
    }

    for i := 0; i < amount; i++ {
        var dest, source reflect.Value

        if isSlice {
            // source
            if from.Kind() == reflect.Slice {
                source = indirect(from.Index(i))
            } else {
                source = indirect(from)
            }
            // dest
            dest = indirect(reflect.New(toType).Elem())
        } else {
            source = indirect(from)
            dest = indirect(to)
        }

        // check source
        if source.IsValid() {
            fromTypeFields := deepFields(fromType)
            // fmt.Printf("%#v", fromTypeFields)
            // Copy from field to field or method
            for _, field := range fromTypeFields {
                name := field.Name
                if skip_field(name) {
                    continue
                }

                if fromField := source.FieldByName(name); fromField.IsValid() {
                    // has field
                    if toField := dest.FieldByName(name); toField.IsValid() {
                        if toField.CanSet() {
                            if !set(toField, fromField) {
                                if err := _copy(toField.Addr().Interface(), fromField.Interface()); err != nil {
                                    return err
                                }
                            }
                        }
                    } else {
                        // try to set to method
                        var toMethod reflect.Value
                        if dest.CanAddr() {
                            toMethod = dest.Addr().MethodByName(name)
                        } else {
                            toMethod = dest.MethodByName(name)
                        }

                        if toMethod.IsValid() && toMethod.Type().NumIn() == 1 && fromField.Type().AssignableTo(toMethod.Type().In(0)) {
                            toMethod.Call([]reflect.Value{fromField})
                        }
                    }
                }
            }

            // Copy from method to field
            for _, field := range deepFields(toType) {
                name := field.Name
                if skip_field(name) {
                    continue
                }

                var fromMethod reflect.Value
                if source.CanAddr() {
                    fromMethod = source.Addr().MethodByName(name)
                } else {
                    fromMethod = source.MethodByName(name)
                }

                if fromMethod.IsValid() && fromMethod.Type().NumIn() == 0 && fromMethod.Type().NumOut() == 1 {
                    if toField := dest.FieldByName(name); toField.IsValid() && toField.CanSet() {
                        values := fromMethod.Call([]reflect.Value{})
                        if len(values) >= 1 {
                            set(toField, values[0])
                        }
                    }
                }
            }
        }
        if isSlice {
            if dest.Addr().Type().AssignableTo(to.Type().Elem()) {
                to.Set(reflect.Append(to, dest.Addr()))
            } else if dest.Type().AssignableTo(to.Type().Elem()) {
                to.Set(reflect.Append(to, dest))
            }
        }
    }
    return
}

func deepFields(reflectType reflect.Type) []reflect.StructField {
    var fields []reflect.StructField

    if reflectType = indirectType(reflectType); reflectType.Kind() == reflect.Struct {
        for i := 0; i < reflectType.NumField(); i++ {
            v := reflectType.Field(i)
            if v.Anonymous {
                fields = append(fields, deepFields(v.Type)...)
            } else {
                fields = append(fields, v)
            }
        }
    }

    return fields
}

func indirect(reflectValue reflect.Value) reflect.Value {
    for reflectValue.Kind() == reflect.Ptr {
        reflectValue = reflectValue.Elem()
    }
    return reflectValue
}

func indirectType(reflectType reflect.Type) reflect.Type {
    for reflectType.Kind() == reflect.Ptr || reflectType.Kind() == reflect.Slice {
        reflectType = reflectType.Elem()
    }
    return reflectType
}

func set(to, from reflect.Value) bool {
    if from.IsValid() {
        if to.Kind() == reflect.Ptr {
            // set `to` to nil if from is nil
            if from.Kind() == reflect.Ptr && from.IsNil() {
                to.Set(reflect.Zero(to.Type()))
                return true
            } else if to.IsNil() {
                to.Set(reflect.New(to.Type().Elem()))
            }
            to = to.Elem()
        }

        if from.Type().ConvertibleTo(to.Type()) {
            to.Set(from.Convert(to.Type()))
        } else if scanner, ok := to.Addr().Interface().(sql.Scanner); ok {
            err := scanner.Scan(from.Interface())
            if err != nil {
                return false
            }
        } else if from.Kind() == reflect.Ptr {
            return set(to, from.Elem())
        } else {
            return false
        }
    }
    return true
}
